package com.whz.generic;

import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 密码相似度计算 ---- 余弦相似度计算, 相似度余弦值越接近1,越相似
 * <p>
 * 默认相似阈值0.9,大于0.9即可认为 目标文本与历史记录相似
 * </p>
 *
 * @author whz
 */
@Slf4j
public class SimilarityEvaluate {
    private double thresholdSimilarity = 0.9D;

    /**
     * 相似度阈值，相似度大于该阈值，认定没有风险，反之亦然
     *
     * @param thresholdSimilarity 相似度阈值
     */
    public SimilarityEvaluate(double thresholdSimilarity) {
        this.thresholdSimilarity = thresholdSimilarity;
        log.info("相似度阈值:{}", thresholdSimilarity);
    }


    /**
     * 匹配目标字符串与历史字符串集合的相似度
     *
     * @param target  目标字符串
     * @param history 历史字符串集合
     * @return 返回true, 目标与历史相似
     */
    public Boolean similarMatch(String target, Set<String> history) {
        /*
         * 没有历史记录不匹配
         */
        if (CollectionUtils.isEmpty(history) || StringUtils.isEmpty(target)) {
            return false;
        }
        //收集历史向量元数据空间
        Set<Character> wordBag = new HashSet<>();
        history.forEach(c -> {
            char[] chars = c.toCharArray();
            for (char aChar : chars) {
                wordBag.add(aChar);
            }
        });
        //收集目标向量元数据
        char[] chars = target.toCharArray();
        for (Character aChar : chars) {
            wordBag.add(aChar);
        }
        //把字符袋中的字符排序
        List<Character> wordList = wordBag.stream()
                                          .sorted()
                                          .collect(Collectors.toList());
        log.info("向量空间:{}", wordList);

        //把字符串字符出现的次数作为向量坐标
        List<Integer[]> historyVector = history.stream()
                                               .map(p -> transformStringToVector(wordList, p))
                                               .collect(Collectors.toList());

        //把目标文本转化为向量
        Integer[] loginVector = transformStringToVector(wordList, target);

        //获取没有风险的相似度集合
        return historyVector.stream().anyMatch(h -> calculateSimilarity(h, loginVector) >= thresholdSimilarity);
    }

    /**
     * 把密码乱序转化为向量
     *
     * @param wordBag  所有历史密码组成的排序好的字符袋
     * @param password 要转化的密码乱序
     * @return 转化后的向量
     */
    public Integer[] transformStringToVector(List<Character> wordBag, String password) {
        //待计算向量
        Integer[] vector = new Integer[wordBag.size()];
        Arrays.fill(vector, 0);

        Map<Character, Integer> passCharMap = Maps.newHashMap();
        //遍历向量空间,统计元数据出现次数
        for (int i = 0; i < wordBag.size(); i++) {
            passCharMap.put(wordBag.get(i), i);
        }
        /*
         * 向量长度统一为元数据各个数,坐标值元素在向量空间的位置, 这样连续的相似的文本有相似的向量
         */
        int i = 0;
        for (Character c : password.toCharArray()) {
            vector[i++] = passCharMap.get(c);
        }

        return vector;
    }

    /**
     * 计算两个向量的余弦值
     *
     * @param v1 v1向量
     * @param v2 v2向量
     * @return 返回相似度
     */
    private Double calculateSimilarity(Integer[] v1, Integer[] v2) {
        //计算两个向量的夹角余弦,向量点乘/模的乘积
        //向量的点乘：v1 * v2
        double dotMulti = 0.0;
        for (int i = 0; i < v1.length; i++) {
            dotMulti += v1[i] * v2[i];
        }
        //计算两个向量的模的平方
        int length1 = Arrays.stream(v1)
                            .map(v -> v * v)
                            .reduce(Integer::sum)
                            .orElse(0);
        int length2 = Arrays.stream(v2)
                            .map(v -> v * v)
                            .reduce(Integer::sum)
                            .orElse(0);

        // 0向量余弦都是1
        if (length1 == 0 || length2 == 0) {
            return 1.0;
        }
        //计算余弦
        return dotMulti / (Math.sqrt(length1) * Math.sqrt(length2));
    }
}